﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace tactics
{
    public class Terrain
    {
        TerrainMap terrainMap;

        VertexPositionNormalTexture[] geometry;
        VertexPositionTexture[] gridGeometry;

        VertexDeclaration vertDecl;
        VertexDeclaration gridVertDecl;

        VertexBuffer vertBuffer;
        VertexBuffer gridVertBuffer;

        int[] indices;
        int[] gridIndices;
        
        IndexBuffer idxBuffer;
        IndexBuffer gridIdxBuffer;

        PrimitiveType primType;

        #region Properties

        public Vector3[] Geometry
        {
            get
            {
                Vector3[] geo = new Vector3[geometry.Length];
                for (int i = 0; i < geometry.Length; ++i )
                {
                    geo[i] = geometry[i].Position;
                }
                return geo;
            }
        }

        public Vector3[] NormalMap
        {
            get
            {
                Vector3[] normals = new Vector3[geometry.Length];
                for (int i = 0; i < geometry.Length; ++i)
                {
                    normals[i] = geometry[i].Normal;
                }
                return normals;
            }
        }

        public VertexPositionNormalTexture[] FullGeometry
        {
            get
            {
                return geometry;
            }
        }

        public VertexPositionTexture[] GridGeometry
        {
            get
            {
                return gridGeometry;
            }
        }

        public VertexBuffer VertexBuffer
        {
            get
            {
                return vertBuffer;
            }
        }

        public VertexBuffer GridVertexBuffer
        {
            get
            {
                return gridVertBuffer;
            }
        }

        public IndexBuffer IndexBuffer
        {
            get
            {
                return idxBuffer;
            }
        }

        public IndexBuffer GridIndexBuffer
        {
            get
            {
                return gridIdxBuffer;
            }
        }

        public VertexDeclaration VertexDeclaration
        {
            get
            {
                return vertDecl;
            }
        }
        
        public VertexDeclaration GridVertexDeclaration
        {
            get
            {
                return gridVertDecl;
            }
        }

        public PrimitiveType PrimitiveType
        {
            get
            {
                return primType;
            }
        }

        public PrimitiveType GridPrimitiveType
        {
            get
            {
                return primType;
            }
        }

        #endregion

        #region Accessors

        public int VertexFromCoord(int x, int y)
        {
            int retval = (y * terrainMap.Width * 6) + (x * 6);
            return retval;
        }

        #endregion

        public Terrain(TerrainMap terrainMapIn)
        {
            terrainMap = terrainMapIn;

            BuildTerrainMesh();
            BuildGridMesh();
        }

        #region Terrain Mesh Creation

        private void BuildVertexBuffer()
        {
            int idx = 0;
            int width = terrainMap.Width;
            int height = terrainMap.Height;
            float scale = terrainMap.Scale;

            Vector3 vert;
            Vector3 normal;
            Vector2 tex;

            geometry = new VertexPositionNormalTexture[(width - 1) * (height - 1) * 12];

            for (int y = 0; y < height - 1; ++y)
            {
                for (int x = 0; x < width - 1; ++x)
                {
                    Vector2 tex1 = terrainMap.GetTexCoordsAt(x, y)[0];
                    Vector2 tex2 = terrainMap.GetTexCoordsAt(x, y)[1];

                    float middle = terrainMap.GetHeight(x, y);
                    float averageHeight = middle;
                    float highest = middle;
                    float lowest = middle;

                    int same = 1;
                    int higher = 0;
                    int lower = 0;

                    float checkHeight = terrainMap.GetHeight(x, y + 1);
                    averageHeight += checkHeight;
                    if (middle == checkHeight)
                    {
                        ++same;
                    }
                    else
                    {
                        if (checkHeight > highest)
                        {
                            highest = checkHeight;
                            ++higher;
                        }
                        else
                        {
                            lowest = checkHeight;
                            ++lower;
                        }
                    }

                    checkHeight = terrainMap.GetHeight(x + 1, y + 1);
                    averageHeight += checkHeight;
                    if (middle == checkHeight)
                    {
                        ++same;
                    }
                    else
                    {
                        if (checkHeight > highest)
                        {
                            highest = checkHeight;
                            ++higher;
                        }
                        else
                        {
                            lowest = checkHeight;
                            ++lower;
                        }
                    }

                    checkHeight = terrainMap.GetHeight(x + 1, y);
                    averageHeight += checkHeight;
                    if (middle == checkHeight)
                    {
                        ++same;
                    }
                    else
                    {
                        if (checkHeight > highest)
                        {
                            highest = checkHeight;
                            ++higher;
                        }
                        else
                        {
                            lowest = checkHeight;
                            ++lower;
                        }
                    }

                    if (!terrainMap.SmoothTerrain)
                    {
                        if (same == 3)
                        {
                            // middle is equal to x/y
                            middle = terrainMap.GetHeight(x, y);
                        }
                        else if (same == 1)
                        {
                            if (higher > lower)
                                middle = highest;
                            else
                                middle = lowest;
                        }
                        else
                        {
                            middle = averageHeight / 4;
                        }
                    }
                    else
                        middle = averageHeight / 4;

                    // left tri

                    vert = new Vector3(x, terrainMap.GetHeight(x, y), y) * scale;
                    tex = new Vector2(tex1.X, tex1.Y) / terrainMap.TerrainGridWidth;
                    normal = new Vector3(0, 0, 0);
                    geometry[idx++] = new VertexPositionNormalTexture(vert, normal, tex);

                    vert = new Vector3(x, terrainMap.GetHeight(x, y + 1), (y + 1)) * scale;
                    tex = new Vector2(tex1.X, tex2.Y) / terrainMap.TerrainGridWidth;
                    normal = new Vector3(0, 0, 0);
                    geometry[idx++] = new VertexPositionNormalTexture(vert, normal, tex);

                    vert = new Vector3(x + 0.5f, middle, y + 0.5f) * scale;
                    tex = new Vector2(tex1.X + 0.5f, tex1.Y + 0.5f) / terrainMap.TerrainGridWidth;
                    normal = new Vector3(0, 0, 0);
                    geometry[idx++] = new VertexPositionNormalTexture(vert, normal, tex);

                    // top tri

                    vert = new Vector3(x, terrainMap.GetHeight(x, y + 1), (y + 1)) * scale;
                    tex = new Vector2(tex1.X, tex2.Y) / terrainMap.TerrainGridWidth;
                    normal = new Vector3(0, 0, 0);
                    geometry[idx++] = new VertexPositionNormalTexture(vert, normal, tex);
                    
                    vert = new Vector3((x + 1), terrainMap.GetHeight(x + 1, y + 1), (y + 1)) * scale;
                    tex = new Vector2(tex2.X, tex2.Y) / terrainMap.TerrainGridWidth;
                    normal = new Vector3(0, 0, 0);
                    geometry[idx++] = new VertexPositionNormalTexture(vert, normal, tex);

                    vert = new Vector3(x + 0.5f, middle, y + 0.5f) * scale;
                    tex = new Vector2(tex1.X + 0.5f, tex1.Y + 0.5f) / terrainMap.TerrainGridWidth;
                    normal = new Vector3(0, 0, 0);
                    geometry[idx++] = new VertexPositionNormalTexture(vert, normal, tex);

                    // right tri

                    vert = new Vector3((x + 1), terrainMap.GetHeight(x + 1, y + 1), (y + 1)) * scale;
                    tex = new Vector2(tex2.X, tex2.Y) / terrainMap.TerrainGridWidth;
                    normal = new Vector3(0, 0, 0);
                    geometry[idx++] = new VertexPositionNormalTexture(vert, normal, tex);

                    vert = new Vector3((x + 1), terrainMap.GetHeight(x + 1, y), y) * scale;
                    tex = new Vector2(tex2.X, tex1.Y) / terrainMap.TerrainGridWidth;
                    normal = new Vector3(0, 0, 0);
                    geometry[idx++] = new VertexPositionNormalTexture(vert, normal, tex);

                    vert = new Vector3(x + 0.5f, middle, y + 0.5f) * scale;
                    tex = new Vector2(tex1.X + 0.5f, tex1.Y + 0.5f) / terrainMap.TerrainGridWidth;
                    normal = new Vector3(0, 0, 0);
                    geometry[idx++] = new VertexPositionNormalTexture(vert, normal, tex);

                    // bottom tri

                    vert = new Vector3((x + 1), terrainMap.GetHeight(x + 1, y), y) * scale;
                    tex = new Vector2(tex2.X, tex1.Y) / terrainMap.TerrainGridWidth;
                    normal = new Vector3(0, 0, 0);
                    geometry[idx++] = new VertexPositionNormalTexture(vert, normal, tex);

                    vert = new Vector3(x, terrainMap.GetHeight(x, y), y) * scale;
                    tex = new Vector2(tex1.X, tex1.Y) / terrainMap.TerrainGridWidth;
                    normal = new Vector3(0, 0, 0);
                    geometry[idx++] = new VertexPositionNormalTexture(vert, normal, tex);

                    vert = new Vector3(x + 0.5f, middle, y + 0.5f) * scale;
                    tex = new Vector2(tex1.X + 0.5f, tex1.Y + 0.5f) / terrainMap.TerrainGridWidth;
                    normal = new Vector3(0, 0, 0);
                    geometry[idx++] = new VertexPositionNormalTexture(vert, normal, tex);
                }
            }
        }

        private void BuildIndexBuffer()
        {
            indices = new int[geometry.Length];

            for (int i = 0; i < indices.Length; ++i)
                indices[i] = i;
        }

        private Vector3 Normal(Vector3 v2, Vector3 v1, Vector3 v3)
        {
            return Vector3.Cross(v2 - v1, v3 - v1);
        }

        private void GenerateNormals()
        {
            for (int i = 0; i < geometry.Length / 3; i++)
            {
                Vector3 firstvec = geometry[i * 3 + 1].Position - geometry[i * 3].Position;
                Vector3 secondvec = geometry[i * 3].Position - geometry[i * 3 + 2].Position;
                Vector3 normal = Vector3.Cross(firstvec, secondvec);
                normal.Normalize();
                geometry[i * 3].Normal += normal;
                geometry[i * 3 + 1].Normal += normal;
                geometry[i * 3 + 2].Normal += normal;
            }

            foreach (var vertex in geometry)
                vertex.Normal.Normalize();
        }

        private void BuildTerrainMesh()
        {
            // build vertex buffer
            BuildVertexBuffer();

            primType = PrimitiveType.TriangleList;

            vertDecl = new VertexDeclaration(TacticsEngine.Instance.GraphicsDevice, VertexPositionNormalTexture.VertexElements);
            vertBuffer = new VertexBuffer(TacticsEngine.Instance.GraphicsDevice, VertexPositionNormalTexture.SizeInBytes * geometry.Length, BufferUsage.WriteOnly);

            // build index buffer
            BuildIndexBuffer();

            idxBuffer = new IndexBuffer(TacticsEngine.Instance.GraphicsDevice, sizeof(int) * indices.Length, BufferUsage.WriteOnly, IndexElementSize.ThirtyTwoBits);
            idxBuffer.SetData(indices);

            // calc normals
            GenerateNormals();
            vertBuffer.SetData(geometry);
        }

        #endregion

        #region Grid Mesh Creation

        void BuildGridVertexBuffer()
        {
            int width = terrainMap.Width;
            int height = terrainMap.Height;
            float scale = terrainMap.Scale;

            gridGeometry = new VertexPositionTexture[geometry.Length];

            for (int y = 0; y < height - 1; ++y)
            {
                for (int x = 0; x < width - 1; ++x)
                {
                    int idx = (y * (width - 1) + x) * 12;

                    gridGeometry[idx] = new VertexPositionTexture(geometry[idx].Position, new Vector2(x, y)); ++idx;
                    gridGeometry[idx] = new VertexPositionTexture(geometry[idx].Position, new Vector2(x, y + 1)); ++idx;
                    gridGeometry[idx] = new VertexPositionTexture(geometry[idx].Position, new Vector2(x + 0.5f, y + 0.5f)); ++idx;
                    gridGeometry[idx] = new VertexPositionTexture(geometry[idx].Position, new Vector2(x, y + 1)); ++idx;
                    gridGeometry[idx] = new VertexPositionTexture(geometry[idx].Position, new Vector2(x + 1, y + 1)); ++idx;
                    gridGeometry[idx] = new VertexPositionTexture(geometry[idx].Position, new Vector2(x + 0.5f, y + 0.5f)); ++idx;
                    gridGeometry[idx] = new VertexPositionTexture(geometry[idx].Position, new Vector2(x + 1, y + 1)); ++idx;
                    gridGeometry[idx] = new VertexPositionTexture(geometry[idx].Position, new Vector2(x + 1, y)); ++idx;
                    gridGeometry[idx] = new VertexPositionTexture(geometry[idx].Position, new Vector2(x + 0.5f, y + 0.5f)); ++idx;
                    gridGeometry[idx] = new VertexPositionTexture(geometry[idx].Position, new Vector2(x + 1, y)); ++idx;
                    gridGeometry[idx] = new VertexPositionTexture(geometry[idx].Position, new Vector2(x, y)); ++idx;
                    gridGeometry[idx] = new VertexPositionTexture(geometry[idx].Position, new Vector2(x + 0.5f, y + 0.5f));
                }
            }
        }

        void BuildGridIndexBuffer()
        {
            gridIndices = new int[gridGeometry.Length];

            for (int i = 0; i < gridIndices.Length; ++i)
                gridIndices[i] = i;
        }

        private void BuildGridMesh()
        {
            // build vertex buffer
            BuildGridVertexBuffer();

            gridVertDecl = new VertexDeclaration(TacticsEngine.Instance.GraphicsDevice, VertexPositionTexture.VertexElements);
            gridVertBuffer = new VertexBuffer(TacticsEngine.Instance.GraphicsDevice, VertexPositionTexture.SizeInBytes * gridGeometry.Length, BufferUsage.WriteOnly);
            gridVertBuffer.SetData(gridGeometry);

            // build index buffer
            BuildGridIndexBuffer();

            gridIdxBuffer = new IndexBuffer(TacticsEngine.Instance.GraphicsDevice, sizeof(int) * gridIndices.Length, BufferUsage.WriteOnly, IndexElementSize.ThirtyTwoBits);
            gridIdxBuffer.SetData(gridIndices);
        }

        #endregion
    }
}
